#region License

// This file is part of Grawlix.
// 
// Grawlix is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
// Grawlix is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public
// License along with Grawlix.  If not, see
// <http://www.gnu.org/licenses/>.

#endregion

using System;
using System.Diagnostics.CodeAnalysis;

namespace Grawlix.Geometry
{
	/// <summary>
	/// An immutable two-dimensional axis-aligned rectangle in a
	/// Cartesian plane.
	/// </summary>
	[Serializable]
	public struct Rect : IEquatable<Rect>, IFormattable
	{
		/// <summary>
		/// The maximal extent (top right corner).
		/// </summary>
		private readonly Vector2 _max;

		/// <summary>
		/// The minimum extent (bottom left corner).
		/// </summary>
		private readonly Vector2 _min;

		/// <summary>
		/// Initializes a new <see cref="Rect"/> structure, given
		/// extents.
		/// </summary>
		/// <param name="x1">The leftmost extent of the rectangle.
		/// </param>
		/// <param name="y1">The bottom extent of the rectangle.
		/// </param>
		/// <param name="x2">The right extent of the rectangle.</param>
		/// <param name="y2">The top extent of the rectangle.</param>
		/// <exception cref="InvalidGeometryException">Width or height
		/// is not zero or positive.</exception>
		[SuppressMessage("Microsoft.Naming", "CA1704",
			Justification =
				"X and Y make sense for Cartesian coordinates.")]
		public Rect(float x1, float y1, float x2, float y2)
			: this(new Vector2(x1, y1), new Vector2(x2, y2))
		{
		}

		/// <summary>
		/// Initializes a new <see cref="Rect"/> structure, given
		/// extents.
		/// </summary>
		/// <param name="minimum">The minimal extent of the rectangle.
		/// </param>
		/// <param name="maximum">The maximal extent of the rectangle.
		/// </param>
		/// <exception cref="InvalidGeometryException">Width or height
		/// is not zero or positive.</exception>
		public Rect(Vector2 minimum, Vector2 maximum)
		{
			_min = minimum;
			_max = maximum;

			if (!(Left <= Right) || !(Bottom <= Top))
			{
				throw new InvalidGeometryException(
					"A rectangle structure must have 0 or positive " +
					"width and height. Rectangle specified is " + this +
					".");
			}
		}

		/// <summary>
		/// The bottommost (lowest Y) extent of the <see cref="Rect"/>.
		/// </summary>
		public float Bottom
		{
			get { return Min.Y; }
		}

		/// <summary>
		/// The centroid of the shape.
		/// </summary>
		/// <remarks>The centroid of a shape is its center of mass,
		/// given uniform density.</remarks>
		public Vector2 Centroid
		{
			get { return (_min + _max) / 2; }
		}

		/// <summary>
		/// A (width, height) pair stored in a <see cref="Vector2"/>
		/// structure.
		/// </summary>
		public Vector2 Dimensions
		{
			get { return Max - Min; }
		}

		/// <summary>
		/// The leftmost (lowest X) extent of the <see cref="Rect"/>.
		/// </summary>
		public float Left
		{
			get { return Min.X; }
		}

		/// <summary>
		/// The maximal (top-right) corner of the <see cref="Rect"/>.
		/// </summary>
		public Vector2 Max
		{
			get { return _max; }
		}

		/// <summary>
		/// The minimal (bottom-left) corner of the <see cref="Rect"/>.
		/// </summary>
		public Vector2 Min
		{
			get { return _min; }
		}

		/// <summary>
		/// The rightmost (highest X) extent of the <see cref="Rect"/>.
		/// </summary>
		public float Right
		{
			get { return Max.X; }
		}

		/// <summary>
		/// The topmost (highest Y) extent of the <see cref="Rect"/>.
		/// </summary>
		public float Top
		{
			get { return Max.Y; }
		}

		/// <summary>
		/// Shifts a <see cref="Rect"/> by a given displacement.
		/// </summary>
		/// <param name="vector">The amount to move in each direction.
		/// </param>
		/// <returns>The shifted <see cref="Rect"/>.</returns>
		public Rect Add(Vector2 vector)
		{
			return this + vector;
		}

		/// <summary>
		/// Checks to see if another <see cref="Rect"/> is within the
		/// current <see cref="Rect"/>.
		/// </summary>
		/// <param name="rect">The <see cref="Rect"/> to check.</param>
		/// <returns>true if within, else false.</returns>
		public bool Contains(Rect rect)
		{
			return rect.Left >= Left && rect.Right <= Right &&
			       rect.Bottom >= Bottom && rect.Top <= Top;
		}

		/// <summary>
		/// Checks to see if a point is within the current 
		/// <see cref="Rect"/>.
		/// </summary>
		/// <param name="point">The point to check.</param>
		/// <returns>true if within, else false.</returns>
		public bool Contains(Vector2 point)
		{
			return point.X >= Left && point.X <= Right &&
			       point.Y >= Bottom && point.Y <= Top;
		}

		/// <summary>
		/// Indicates whether this instance and a specified object are
		/// equal.
		/// </summary>
		/// <returns>
		/// true if <paramref name="obj" /> and this instance are the
		/// same type and represent the same value; otherwise, false.
		/// </returns>
		/// <param name="obj">Another object to compare to. </param>
		public override bool Equals(object obj)
		{
			return obj.GetType() == typeof(Rect) && Equals((Rect)obj);
		}

		/// <summary>
		/// Indicates whether the current object is equal to another
		/// object of the same type.
		/// </summary>
		/// <returns>
		/// true if the current object is equal to the
		/// <paramref name="other" /> parameter; otherwise, false.
		/// </returns>
		/// <param name="other">An object to compare with this object.
		/// </param>
		public bool Equals(Rect other)
		{
			return other._max.Equals(_max) && other._min.Equals(_min);
		}

		/// <summary>
		///Returns the hash code for this instance.
		/// </summary>
		/// <returns>
		///A 32-bit signed integer that is the hash code for this
		///instance.
		/// </returns>
		public override int GetHashCode()
		{
			unchecked
			{
				return (_max.GetHashCode() * 397) ^ _min.GetHashCode();
			}
		}

		/// <summary>
		/// Projects a displacement vector from this <see cref="Rect"/>
		/// to another.
		/// </summary>
		/// <param name="toProject">The displacement vector to project.
		/// </param>
		/// <param name="target">The target <see cref="Rect"/> to
		/// project to.</param>
		/// <returns>The projected displacement vector.</returns>
		/// <remarks><para>The difference between point projection and
		/// displacement projection is whether absolution position is
		/// taken into effect.
		/// </para>
		/// <para>
		/// A displacement vector will only be scaled by the dimensions
		/// of the <see cref="Rect"/>, whereas a point vector will
		/// maintain its relative position to all four corners of the 
		/// <see cref="Rect"/>.
		/// </para>
		/// </remarks>
		public Vector2 ProjectDisplacement(Vector2 toProject,
		                                   Rect target)
		{
			toProject = toProject.ArrayDivide(Dimensions);
			toProject = toProject.ArrayMultiply(target.Dimensions);
			return toProject;
		}

		/// <summary>
		/// Projects a point vector from this <see cref="Rect"/> to
		/// another.
		/// </summary>
		/// <param name="toProject">The point vector to project.
		/// </param>
		/// <param name="target">The target <see cref="Rect"/> to
		/// project to.</param>
		/// <returns>The projected point vector.</returns>
		/// <remarks><para>The difference between point projection and
		/// displacement projection is whether absolution position is
		/// taken into effect.
		/// </para>
		/// <para>
		/// A displacement vector will only be scaled by the dimensions
		/// of the <see cref="Rect"/>, whereas a point vector will
		/// maintain its relative position to all four corners of the 
		/// <see cref="Rect"/>.
		/// </para>
		/// </remarks>
		public Vector2 ProjectPoint(Vector2 toProject, Rect target)
		{
			toProject -= Min;
			toProject = toProject.ArrayDivide(Dimensions);
			toProject = toProject.ArrayMultiply(target.Dimensions);
			toProject += target.Min;
			return toProject;
		}

		/// <summary>
		/// Projects a <see cref="Rect"/> from the current one to
		/// another.
		/// </summary>
		/// <param name="toProject">The <see cref="Rect"/> to project.
		/// </param>
		/// <param name="target">The target <see cref="Rect"/> to
		/// project to.</param>
		/// <returns>The projected rectangle.</returns>
		/// <remarks>
		/// The projected <see cref="Rect"/> will maintain its relative
		/// position to the target <see cref="Rect"/>.
		/// </remarks>
		public Rect ProjectRect(Rect toProject, Rect target)
		{
			return new Rect(ProjectPoint(toProject.Min, target),
			                ProjectPoint(toProject.Max, target));
		}

		/// <summary>
		/// Shifts a <see cref="Rect"/> by a given negated
		/// displacement.
		/// </summary>
		/// <param name="vector">
		/// The negated amount to move in each direction.</param>
		/// <returns>The shifted <see cref="Rect"/>.</returns>
		public Rect Subtract(Vector2 vector)
		{
			return this - vector;
		}

		/// <summary>
		/// Obtains a string representation of this object.
		/// </summary>
		/// <returns>A string representation of this object.</returns>
		public override string ToString()
		{
			return ToString(null, null);
		}

		/// <summary>
		/// Formats the value of the current instance using numbers in
		/// the specified format.
		/// </summary>
		/// <returns>
		/// A <see cref="T:System.String" /> containing the value of
		/// the current instance in the specified format.
		/// </returns>
		/// <param name="format">The <see cref="T:System.String" />
		/// specifying the number format to use, or null to specify the
		/// default.
		/// </param>
		/// <param name="formatProvider">The
		/// <see cref="T:System.IFormatProvider" /> to use to format
		/// the value, or null to obtain the numeric format information
		/// from the current locale setting of the operating system. 
		/// </param>
		public string ToString(string format,
		                       IFormatProvider formatProvider)
		{
			return "(" + Min.ToString(format, formatProvider) + "; " +
			       Max.ToString(format, formatProvider) + ")";
		}

		/// <summary>
		/// Shifts a <see cref="Rect"/> by a given displacement.
		/// </summary>
		/// <param name="rect">The area to move.</param>
		/// <param name="vector">The amount to move in each direction.
		/// </param>
		/// <returns>The shifted <see cref="Rect"/>.</returns>
		public static Rect operator +(Rect rect, Vector2 vector)
		{
			return new Rect(rect.Min + vector, rect.Max + vector);
		}

		/// <summary>
		/// Shifts a <see cref="Rect"/> by a given displacement.
		/// </summary>
		/// <param name="rect">The area to move.</param>
		/// <param name="vector">The amount to move in each direction.
		/// </param>
		/// <returns>The shifted <see cref="Rect"/>.</returns>
		public static Rect operator +(Vector2 vector, Rect rect)
		{
			return rect + vector;
		}

		/// <summary>
		/// Determines whether two <see cref="Rect"/> objects have the
		/// same value.
		/// </summary>
		/// <param name="left">A <see cref="Rect"/>.</param>
		/// <param name="right">A <see cref="Rect"/>.</param>
		/// <returns>true if the value of <paramref name="left"/> is
		/// the same as <paramref name="right"/>; else false.</returns>
		public static bool operator ==(Rect left, Rect right)
		{
			return left.Equals(right);
		}

		/// <summary>
		/// Determines whether two <see cref="Rect"/> objects have
		/// different values.
		/// </summary>
		/// <param name="left">A <see cref="Rect"/>.</param>
		/// <param name="right">A <see cref="Rect"/>.</param>
		/// <returns>true if the value of <paramref name="left"/> is
		/// different from <paramref name="right"/>; else false.
		/// </returns>
		public static bool operator !=(Rect left, Rect right)
		{
			return !left.Equals(right);
		}

		/// <summary>
		/// Shifts a <see cref="Rect"/> by a given negated
		/// displacement.
		/// </summary>
		/// <param name="rect">The area to move.</param>
		/// <param name="vector">
		/// The negated amount to move in each direction.</param>
		/// <returns>The shifted <see cref="Rect"/>.</returns>
		public static Rect operator -(Rect rect, Vector2 vector)
		{
			return new Rect(rect.Min - vector, rect.Max - vector);
		}
	}
}